<template>
  <div class="fx-affix" ref="affixDom" :style="affixStyle">
    <div class="fx-affix-content" ref="affixContentDom" :style="affixContentStyle">
      <slot />
    </div>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { PropType, SetupContext } from 'vue'

interface AffixProps {
  affix: boolean
  offset: number
  target: string
  scrollTarget: string
  position: 'bottom' | 'top'
  zIndex: number
}

export default defineComponent({
  name: 'fxAffix',
  props: {
    // 是否需要固定
    affix: {
      type: Boolean as PropType<boolean>,
      default: true
    },
    // 偏移距离
    offset: {
      type: Number as PropType<number>,
      default: 100
    },
    // 指定容器
    target: {
      type: String as PropType<string>,
      default: null
    },
    // 指定容器
    scrollTarget: {
      type: String as PropType<string>,
      default: 'body'
    },
    // 定位方向
    position: {
      type: String as PropType<'bottom' | 'top'>,
      default: 'top',
      validator(val: string) {
        return ['bottom', 'top'].includes(val)
      }
    },
    // 层级
    zIndex: {
      type: Number as PropType<number>,
      default: 100
    }
  },
  emits: ['change'],
  setup(props: AffixProps, ctx: SetupContext) {
    // 是否固钉
    const isAffix = ref<boolean>(false)
    // 固钉父级DOM
    const affixDom = ref<HTMLElement>()
    // 固钉DOM
    const affixContentDom = ref<HTMLElement>()
    // 固钉父级样式
    const affixStyle = computed(() => {
      if (!isAffix.value || !affixDom.value) return null
      const rect = affixDom.value?.getBoundingClientRect()
      return {
        width: `${rect.width}px`,
        height: `${rect.height}px`
      }
    })
    // 固钉样式
    const affixContentStyle = computed(() => {
      if (!isAffix.value || !affixDom.value) return null
      const { left } = affixDom.value?.getBoundingClientRect()
      return {
        position: 'fixed',
        left: `${left}px`,
        top: props.position === 'top' ? `${props.offset}px` : null,
        bottom: props.position === 'bottom' ? `${props.offset}px` : null,
        zIndex: props.zIndex
      }
    })

    // 监听固钉变化
    watch(isAffix, (val: boolean) => {
      ctx.emit('change', val)
    })

    // 滚动事件
    const handleScroll = () => {
      if (!props.affix || !affixDom.value || !affixContentDom.value) return
      const rect = affixDom.value?.getBoundingClientRect()
      const clientHeight = document.documentElement.clientHeight
      if (props.target) {
        isAffix.value = rect.top < props.offset
        const target = document.querySelector(props.target) as HTMLElement
        const targetRect = target.getBoundingClientRect()
        const targetScroll = targetRect.top - rect.height - props.offset
        if (targetScroll < 0 && Math.abs(targetScroll) > targetRect.height) {
          affixContentDom.value.style.transform = `translateY(-${Math.abs(targetScroll) - targetRect.height}px)`
        } else {
          affixContentDom.value.style.transform = 'translateY(0)'
        }
        return
      }
      if (props.position === 'bottom') {
        isAffix.value = rect.top + rect.height > clientHeight - props.offset
      } else if (props.position === 'top') {
        isAffix.value = rect.top < props.offset
      }
    }

    onMounted(() => {
      handleScroll()
      const scrollTarget = document.querySelector(props.scrollTarget) as HTMLElement
      scrollTarget.addEventListener('scroll', handleScroll)
    })

    onBeforeUnmount(() => {
      const scrollTarget = document.querySelector(props.scrollTarget) as HTMLElement
      scrollTarget.removeEventListener('scroll', handleScroll)
    })

    return {
      isAffix,
      affixDom,
      affixContentDom,
      affixStyle,
      affixContentStyle
    }
  }
})
</script>
<style>
.fx-affix {
  display: inline-block;
}
</style>
